DLO-JZ Data parallism ZeRO et Pipeline parallelism - Jour 4¶

Comparatif des différents types de parallèlisme sur un gros Vision Transformer CoAtNet.

Monstertruck

Objet du notebook¶

Le but de ce notebook est d'optimiser un code d'apprentissage d'un modèle CoAtNet-7 sur Imagenet pour Jean Zay en implémentant :

  • TP 1 : Passage à CoAtNet
  • TP 2 : Pipeline parallelism avec PyTorch
  • TP 3 : Deepspeed - ZeRo Data Parallelism
  • TP 4 : Deepspeed - Pipeline Parallelism et comparatif

Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.

Les directives de modification seront marquées par l'étiquette TODO : dans le notebook suivant.

Les solutions sont présentes dans le répertoire solutions/.

Notebook rédigé par l'équipe assistance IA de l'IDRIS, juin 2023


Environnement de calcul¶

Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :

In [1]:
!module list
Currently Loaded Modulefiles:
 1) cuda/11.2                5) openmpi/4.1.1-cuda   9) sparsehash/2.0.3        
 2) nccl/2.9.6-1-cuda        6) intel-mkl/2020.4    10) libjpeg-turbo/2.1.3     
 3) cudnn/8.1.1.33-cuda      7) magma/2.5.4-cuda    11) pytorch-gpu/py3/1.11.0  
 4) gcc/8.5.0(8.3.1:8.4.1)   8) sox/14.4.2          
>

Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.

Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.

TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation.

In [2]:
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, pipe_memory, turbo_profiler
MODULE = 'pytorch-gpu/py3/1.13.0'
image_size = 224
account = 'for@v100'
name = 'pseudo'   ## TODO Pseudonyme à choisir

Gestion de la queue SLURM¶

Cette partie permet d'afficher et de gérer la queue SLURM.

Pour afficher toute la queue utilisateur :

In [3]:
display_slurm_queue()
 Done!

Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.

Si vous voulez arrêter des jobs dans la queue :

  • Annuler tous vos jobs dans la queue (décommenter la ligne suivante)
  • Annuler un job dans votre queue (décommenter la ligne suivante et ajouter le numéro du job à la fin de la ligne)
In [4]:
#!scancel -u $USER

Debug¶

Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.

Il est nécessaire dans la cellule suivante (en décommentant) d'indiquer le jobid correspondant sous le format suivant.

*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée.

In [94]:
jobid = ['208820']

Fichier de sortie :

In [95]:
%cat {search_log(contains=jobid[0])[0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0])[0]}'

Fichier d'erreur :

In [96]:
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}'

Différence de scripts ¶

Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.

In [22]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz4_1.py"
compare(s1, s2)

Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :

compare.html


TP4_0 : Préparation¶

TODO : copier-coller la solution solutions/dlojz4_0.py dans dlojz.py afin d'ajouter dans le code les 2 éléments suivants nécessaires pour la suite des TP :

  • utiliser une taille d'image équivalente pour la validation et le training car CoatNet n'a pas la même souplesse que ResNet, il nécessite une même taille d'image (multiple de 32).
  • afficher dans les logs l'empreinte mémoire de tous les GPU.

À noter : Pendant tout le TP, nous utiliserons une taille d'image de 352 x 352, qui correspond à la taille classique utilisée pour ce modèle.

Pour visualiser ces changements, veuillez utiliser le différentiel de fichiers suivant.

In [21]:
s1 = "dlojz.py"
s2 = "./solutions/dlojz4_0.py"
compare(s1, s2)

compare.html

In [20]:
# copier/coller la solution si nécessaire
!cp solutions/dlojz4_0.py dlojz.py

TP4_1 : CoAtNet¶

Ce TP consiste à lister les versions du modèle CoATNet, de l'appliquer à notre code et de juger des problématques liées aux gros modèles.

Liste des versions de CoAtNet¶

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [11]:
n_gpu = 1
batch_size = 32
command = f'CoAtNet/coatnet.py'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248347
jobid = ['248347']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [12]:
#jobid = ['1790096']
In [13]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248347   gpu_p13   pseudo  cfor132  R       1:10      1 r6i3n0

 Done!
In [14]:
%cat {search_log(name, contains=jobid[0])[0]}
CoAtNet 0: output shape = torch.Size([1, 1000]), N of Parameters = 20918064
CoAtNet 1: output shape = torch.Size([1, 1000]), N of Parameters = 39890496
CoAtNet 2: output shape = torch.Size([1, 1000]), N of Parameters = 64652672
CoAtNet 3: output shape = torch.Size([1, 1000]), N of Parameters = 131079872
CoAtNet 4: output shape = torch.Size([1, 1000]), N of Parameters = 228407456
CoAtNet 5: output shape = torch.Size([1, 1000]), N of Parameters = 610269344
CoAtNet 6: output shape = torch.Size([1, 1000]), N of Parameters = 1453918848
CoAtNet 7: output shape = torch.Size([1, 1000]), N of Parameters = 2362119808
Mon Jun 26 01:22:31 CEST 2023

CoAtNet-6¶

TODO : dans le script dlojz.py :

  • Importer la description des architectures CoAtNet.
from CoAtNet.coatnet import coatnet_6
  • Remplacer :

    model = models.resnet50() par model = coatnet_6((args.image_size,args.image_size))

    et

    archi_model = 'Resnet-50' par archi_model = 'CoAtNet-6'

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [23]:
n_gpu = 4
batch_size = 4
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 248359
jobid = ['248359']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculler la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [24]:
#jobid = ['1790206']
In [25]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248359   gpu_p13   pseudo  cfor132  R       2:45      1 r6i7n6

 Done!
In [26]:
controle_technique(jobid)
Train throughput: 37.26 images/second
GPU throughput: 37.28 images/second
epoch time: 34383.10 seconds
training time estimation for 90 epochs (with validations): 863.63 hours
-----------
training step time average (fwd/bkwd on GPU): 0.429143 sec (23.9%/75.6%) +/- 0.029793
loading step time average (CPU to GPU): 0.000254 sec +/- 0.000031
-----------
ELIGIBLE to run 0 epochs

Test d'occupation mémoire¶

Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [27]:
n_gpu = 1
batch_size = [2, 4, 6, 8]
command = [f'dlojz.py -b {b} --image-size {image_size} --test'
          for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task
Submitted batch job 248362
Submitted batch job 248363
Submitted batch job 248364
Submitted batch job 248365
jobids = ['248362', '248363', '248364', '248365']

Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [28]:
#jobids = ['1790142', '1790143', '1790144', '1790146']
In [29]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248364   gpu_p13   pseudo  cfor132  R       2:01      1 r6i3n3

 Done!
In [30]:
GPU_underthehood(jobids)
Batch size per GPU: 2 Max GPU Memory Allocated: 27.27 GB, Troughput: 6.460 images/second
Batch size per GPU: 4 Max GPU Memory Allocated: 28.08 GB, Troughput: 11.127 images/second
Batch size per GPU: 6 Max GPU Memory Allocated: 29.83 GB, Troughput: 14.797 images/second
Batch size per GPU: 8 CUDA out of memory
Memory occupancy by Model part : 26.837 +/- 0.376 GB

Commentaires


TP4_2 : Pipelined Parallelism de PyTorch¶

Ce TP consiste à implémenter le Pipelined Parallelism de PyTorch et de comparer cette solution avec les autres solutions.

La principale contrainte induite est de structurer le modèle comme suit, avec des torch.nn.Sequential pour chaque section et pour le modèle entier :

pipeline pytorch

Le Pipeline Parallelism de PyTorch est de type standard GPipe.

G Pipe

À noter : Le code modifié permettra de faire de l'Hybrid Parallelism (DP + PP).

Chaque instance créée par Data Parallelism sera associée à une task Slurm, et chacune de ces instances pourra elle-même sollliciter plusieurs GPU pour tourner en mode Pipelined Parallelism.

Dans notre cas, nous testerons le code seulement en mode Pipelined Parallelism, sur 1 task associée à 4 GPU.

TODO : dans le script dlojz.py:

  • Importer les fonctions nécessaires.
from torch.distributed.pipeline.sync import Pipe
import tempfile
from torch.distributed import rpc
  • Ajouter l'argument --chunks (pour le nombre de micro batches) avant le parser les arguments.
parser.add_argument('--chunks', default=1, type=int, help='number of chunks for Pipelined Parallelism')

args = parser.parse_args()
  • Initialiser le Framework RPC, juste après la configuration de la distribution.
# Initialize RPC Framework, Pipe depends on it
tmpfile = tempfile.NamedTemporaryFile()
rpc.init_rpc(
    name=f'worker{idr_torch.rank}',
    rank=0,
    world_size=1,
    rpc_backend_options=rpc.TensorPipeRpcBackendOptions(
        init_method="file://{}".format(tmpfile.name),
        # Specifying _transports and _channels is a workaround and we no longer
        # will have to specify _transports and _channels for PyTorch 
        # versions >= 1.8.1 (Not True for Jean Zay)
        # With Jean Zay, _transports must be equal to ["shm", "uv"] and not ["ibv", "uv"]
        _transports=["shm", "uv"],
        _channels=["cuda_ipc", "cuda_basic"],
    )
)
  • Structurer le modèle pour le Pipelined Parallelism.
# define model
model = coatnet_6((args.image_size,args.image_size))

# How many sections
nb_part = torch.cuda.device_count()//int(os.environ['SLURM_NTASKS_PER_NODE']) 
# device number where the first part of the model will run
first_part = idr_torch.local_rank*nb_part
# list of devices involved for pipelined Parallelism
gpus = [g for g in range(first_part, first_part+nb_part)]

class LambdaModule(torch.nn.Module):
    def __init__(self, lambd):
        super().__init__()
        assert isinstance(lambd, type(lambda x: x))
        self.lambd = lambd

    def forward(self, x):
        return self.lambd(x)

lambda_fc = LambdaModule(lambda x: x.view(-1, 2048))

section0 = torch.nn.Sequential(*model.s0, *model.s1, *model.s2, *model.pres3).to(gpus[0])
section1 = torch.nn.Sequential(*model.s3[:15]).to(gpus[1])
section2 = torch.nn.Sequential(*model.s3[15:30]).to(gpus[2])
section3 = torch.nn.Sequential(*model.s3[30:], *model.s4, model.pool, lambda_fc, model.fc).to(gpus[3])
pipe_model = torch.nn.Sequential(*section0, *section1, *section2, *section3)

# Pipe the model, chunks=n means that the batch (size according to batch size) will be shared to n micro batches (size = batch_size/chunks)
model = Pipe(pipe_model, chunks=args.chunks, checkpoint="never")

archi_model = 'CoAtNet-6'
  • Modifier la déclaration du DistributedDataParallel pour prendre en compte le fait qu'il y a plusieurs GPU associés à une seule task pour le Pipelined Parallelism, en indiquant simplement :
model = DistributedDataParallel(model)
  • Envoyer les métriques de validation au dernier device du Pipe.
## Initialisation  
if idr_torch.rank == 0: accuracies = []
val_loss = torch.Tensor([0.]).to(gpus[-1])                  # send to GPU
val_accuracy = torch.Tensor([0.]).to(gpus[-1])              # send to GPU
  • Dans les boucles de training et de validation, envoyer les Input/images au premier GPU et les labels au dernier GPU.
# distribution of images and labels to all GPUs
images = images.to(gpus[0], non_blocking=args.non_blocking)
labels = labels.to(gpus[-1], non_blocking=args.non_blocking)

et

# distribution of images and labels to all GPUs
val_images = val_images.to(gpus[0], non_blocking=args.non_blocking)
val_labels = val_labels.to(gpus[-1], non_blocking=args.non_blocking)
  • La sortie du modèle Pipelined est au format Rref, il faudra utiliser la méthode .local_value() pour le transformer en tenseur pour le calcul de la loss, dans les boucles de training et de validation.
# Runs the forward pass with autocasting.
with autocast():
    outputs = model(images).local_value()
    loss = criterion(outputs, labels)

et

# Runs the forward pass with no grade mode.
with torch.no_grad():
    with autocast():
        val_outputs = model(val_images).local_value()
        loss = criterion(val_outputs, val_labels)
  • Ajouter pour les logs, la mesure de l'empreinte mémoire sur tous les GPU avec la ligne suivante après la boucle d'apprentissage.
else:                                                                                                          #
    print(f'MaxMemory for GPU:{idr_torch.rank} {torch.cuda.max_memory_allocated()} Bytes')                                   #
#***************************************************************************************************************
for g in gpus: print(f'MaxMemory for GPU:{g} {torch.cuda.max_memory_allocated(device=g)} Bytes')

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [31]:
n_gpu = 4
batch_size = 32 
command = f'dlojz.py -b {batch_size} --image-size {image_size} --chunks 4 --test'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name, n_gpu_per_task=4, 
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 1 tasks / 4 gpus per node and 40 cpus per task
Submitted batch job 248369
jobid = ['248369']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculler la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.

In [32]:
#jobid = ['1790229']
In [33]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248369   gpu_p13   pseudo  cfor132  R       4:44      1 r6i7n6

 Done!
In [34]:
controle_technique(jobid)
Train throughput: 13.66 images/second
GPU throughput: 13.66 images/second
epoch time: 93819.94 seconds
training time estimation for 90 epochs (with validations): 2375.62 hours
-----------
training step time average (fwd/bkwd on GPU): 2.343055 sec (33.5%/68.4%) +/- 0.060034
loading step time average (CPU to GPU): 0.000276 sec +/- 0.000030
-----------
ELIGIBLE to run 1 epochs
In [35]:
pipe_memory(jobid)

Test d'occupation mémoire¶

Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [36]:
n_gpu = 4
chunks = [(16,2), (32,4), (48,6), (56,7), (64,8)]
command = [f'dlojz.py -b {c[0]} --image-size {image_size} --chunks {c[1]} --test'
          for c in chunks]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,  n_gpu_per_task=4, 
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 1 tasks / 4 gpus per node and 40 cpus per task
Submitted batch job 248393
Submitted batch job 248394
Submitted batch job 248396
Submitted batch job 248397
Submitted batch job 248398
jobids = ['248393', '248394', '248396', '248397', '248398']

Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [37]:
#jobids = ['1790423', '1790425', '1790428', '1790429', '1790430']
In [38]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248397   gpu_p13   pseudo  cfor132  R       5:43      1 r7i2n2

 Done!
In [39]:
GPU_underthehood(jobids, calcul_memo=True)
Batch size per GPU: 16 Max GPU Memory Allocated: 8.38 GB, Troughput: 10.052 images/second
Batch size per GPU: 32 Max GPU Memory Allocated: 15.74 GB, Troughput: 13.239 images/second
Batch size per GPU: 48 Max GPU Memory Allocated: 23.21 GB, Troughput: 14.667 images/second
Batch size per GPU: 56 Max GPU Memory Allocated: 26.94 GB, Troughput: 15.250 images/second
Batch size per GPU: 64 CUDA out of memory
Memory occupancy by Model part : 6.511 +/- 5.498 GB
In [40]:
controle_technique([jobids[-2]])
Train throughput: 15.25 images/second
GPU throughput: 15.25 images/second
epoch time: 84018.02 seconds
training time estimation for 90 epochs (with validations): 2128.01 hours
-----------
training step time average (fwd/bkwd on GPU): 3.672140 sec (32.7%/66.7%) +/- 0.038428
loading step time average (CPU to GPU): 0.000297 sec +/- 0.000050
-----------
ELIGIBLE to run 1 epochs

Commentaires


TP4_3 : Deepspeed¶

Préparation¶

Il faut enlever le Pipelined Parallelism du fichier dlojz.py. Nous vous proposons de copier-coller la solution du TP4_1 pour revenir à l'état précédent.

TODO :

  • Copier-coller la solution solutions/dlojz4_1.py dans le fichier dlojz.py
In [41]:
# copier/coller la solution si nécessaire
!cp solutions/dlojz4_1.py dlojz.py

Implémentation de deepspeed¶

Ce TP consiste à implémenter Deepspeed pour intégrer l'optimisation ZeRO pour le Data Parallelism.

TODO : dans le script dlojz.py :

  • Importer Deepspeed.
import deepspeed
  • Intégrer la configuration de Deepspeed par fichier de configuration json dans le parser d'arguments.
# Include DeepSpeed configuration arguments
parser = deepspeed.add_config_arguments(parser)
  • Remplacer le mécanisme de distribution de PyTorch par celui de Deepspeed :

À la place de :

# configure distribution method: define rank and initialise communication backend (NCCL)
dist.init_process_group(backend='nccl', init_method='env://',
                        world_size=idr_torch.size, rank=idr_torch.rank)
...
model = model.to(gpu)
...
model = DistributedDataParallel(model, device_ids=[idr_torch.local_rank])
...

mettre :

# Deepspeed initialization - force port number if several job run on the same node 
deepspeed.init_distributed(distributed_port=os.environ['MASTER_PORT'])
model_engine, optimizer, _, scheduler = deepspeed.initialize(args=args,
                                                     model=model, 
                                                     model_parameters=model.parameters()
                                                     )

À noter : Nous garderons, comme indiqué dans la documentation de Deepspeed, la distinction entre le modèle PyTorch model et le modèle encapsulé avec Deepspeed model_engine.

  • Appliquer le nouveau modèle dans l'étape de forward.
outputs = model_engine(images)

et

val_outputs = model_engine(val_images)
  • Désactiver l'AMP.

En effet, l'optimisation ZeRO ne supporte pas l'Automatic Mixed Precision. À la place, on appliquera une précision float16 à l'ensemble des paramètres du modèle (cela se fera dans la configuration json).

Pour retrouver les lignes de code à modifier dans le script dlojz.py, vous pouvez utiliser l'outil de différentiel de texte entre la solution dlojz1_1.py et la solution dlojz1_2.py.

  • Caster les données d'entrée en float16 afin qu'elles correspondent à la précision du modèle :
images = images.half().to(gpu, non_blocking=args.non_blocking, memory_format=torch.channels_last)

et

val_images = val_images.half().to(gpu, non_blocking=args.non_blocking, memory_format=torch.channels_last)
  • Déléguer les étapes de backward et d'actualisation des poids à Deepspeed dans la boucle de training en remplaçant :
# backward and optimize
loss.backward()
optimizer.step()

par

#runs backpropagation
model_engine.backward(loss)

#weight update
model_engine.step()
  • Effacer ou commenter le bloc suivant, puisque l'on utilisera le mécanisme de Deepspeed pour le learning rate scheduler :
# scheduler update
#scheduler.step()

Configuration de ZeRO¶

La configuration de Deepspeed se fait par fichier JSON :

In [97]:
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 16,
  "gradient _accumulation_steps": 1,
  
  "optimizer": {
    "type": "Adam",
    "params": {
      "lr": 0.001,
      "weight_decay": 5e-4
    }
  },
 
  "scheduler": {
      "type": "OneCycle",
      "params": {
          "cycle_min_lr": 1e-6,
          "cycle_max_lr": 1e-3,
          "decay_lr_rate": 1e-6
      }
  },
 
  "fp16": {
    "enabled": true,
    "loss_scale": 0,
    "initial_scale_power": 32,
    "loss_scale_window": 1000,
    "hysteresis": 2,
    "min_loss_scale": 1
    },
 
 "zero_optimization": {
    "stage": 2
 },
 "zero_allow_untested_optimizer": true
}
Overwriting ds_config.json

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [98]:
n_gpu = 4
batch_size = 16
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 248510
jobid = ['248510']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [99]:
#jobid = ['248508']
In [100]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248511    gpu_p5   pseudo  cfor132  R       0:46      1 jean-zay-iam01
Key interrupt
In [101]:
controle_technique(jobid)
Train throughput: 94.35 images/second
GPU throughput: 94.93 images/second
epoch time: 13579.50 seconds
training time estimation for 90 epochs (with validations): 343.32 hours
-----------
training step time average (fwd/bkwd on GPU): 0.674188 sec (26.7%/66.0%) +/- 0.061517
loading step time average (CPU to GPU): 0.004142 sec +/- 0.002663
-----------
ELIGIBLE to run 1 epochs
In [102]:
pipe_memory(jobid)

Test d'occupation mémoire¶

Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.

In [64]:
import json
batch_size = [2, 4, 8, 16, 24, 32]
for b in batch_size:
    with open("ds_config.json", "r") as jsonFile:
        data = json.load(jsonFile)

    data["train_micro_batch_size_per_gpu"] = b

    with open(f"ds_config{b}.json", "w") as jsonFile:
        json.dump(data, jsonFile)

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [65]:
n_gpu = 4
command = [f'dlojz.py -b {b} --image-size {image_size} --test --deepspeed --deepspeed_config ds_config{b}.json'
          for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 248497
Submitted batch job 248498
Submitted batch job 248499
Submitted batch job 248500
Submitted batch job 248502
Submitted batch job 248503
jobids = ['248497', '248498', '248499', '248500', '248502', '248503']

Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [66]:
#jobids = ['169112', '169113', '169114', '169115', '169116', '169117']
In [67]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248502   gpu_p13   pseudo  cfor132  R       2:51      1 r8i2n6

 Done!
In [68]:
GPU_underthehood(jobids)
Batch size per GPU: 8 Max GPU Memory Allocated: 11.76 GB, Troughput: 26.472 images/second
Batch size per GPU: 16 Max GPU Memory Allocated: 11.76 GB, Troughput: 46.642 images/second
Batch size per GPU: 32 Max GPU Memory Allocated: 13.53 GB, Troughput: 72.730 images/second
Batch size per GPU: 64 Max GPU Memory Allocated: 19.12 GB, Troughput: 93.009 images/second
Batch size per GPU: 96 Max GPU Memory Allocated: 24.85 GB, Troughput: 103.909 images/second
Batch size per GPU: 128 CUDA out of memory
Memory occupancy by Model part : 9.899 +/- 1.562 GB
In [69]:
controle_technique([jobids[-2]])
Train throughput: 103.39 images/second
GPU throughput: 103.91 images/second
epoch time: 12391.75 seconds
training time estimation for 90 epochs (with validations): 313.71 hours
-----------
training step time average (fwd/bkwd on GPU): 0.923888 sec (29.2%/64.8%) +/- 0.069357
loading step time average (CPU to GPU): 0.004611 sec +/- 0.002797
-----------
ELIGIBLE to run 1 epochs

Commentaires


TP3_4 : Pipeline Parallelism avec Deepspeed¶

Ce TP consite à implémenter le Pipeline Parallelism de Deepspeed que l'on pourra ensuite utiliser en mode hybride avec le Data Parallelism + ZeRO.

La version du Pipelined Parallelism de Deepspeed est optimisé pour économiser l'empreinte mémoire.

pipeline deepspeed

À noter : Avec Deepspeed, le Pipelined Parallelism comme le Data Parallism fonctionne toujours en multi-task, ainsi une task est associée à chaque device.

L'implémentation du Pipeline Parallelism amenant trop de changements par rapport au code manipulé durant le TP, nous vous suggérons de copier-coller la solution solutions/dlojz4_4.py sur dlojz.py.

TODO :

  • Copier-coller solutions/dlojz4_4.py sur dlojz.py.
  • Regarder le code. Notamment :
# Define Pipeline Module
deepspeed.init_distributed(distributed_port=os.environ['MASTER_PORT'])
model = PipelineModule(layers = [
                    *model.s0, *model.s1, *model.s2, *model.pres3, *model.s3, *model.s4,
                     model.pool, lambda x: x.view(-1, 2048), model.fc],
                     num_stages = args.nb_pipeline_stages,
                     loss_fn=criterion,
                     partition_method = 'parameters' if args.partition_param else 'uniform')

# Deepspeed initialization - force port number if several job run on the same node 
model_engine, optimizer, _, scheduler = deepspeed.initialize(args=args,
                                                     model=model, 
                                                     model_parameters=model.parameters(),
                                                     training_data=train_dataset)
...

    loss = model_engine.train_batch()
....    

    val_loss = model_engine.eval_batch(val_iter)

Configuration JSON :¶

À noter : la configuration du Pipeline Parallelism se fait avec :

  • train_micro_batch_size_per_gpu correspondant à la taille du micro batch,
  • gradient_accumulation_steps correspondant au nombre de tronçons du pipeline.

La taille du mini batch pour chaque itération d'apprentissage correspond donc à train_micro_batch_size_per_gpu x gradient_accumulation_steps.

In [103]:
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 24,
  "gradient_accumulation_steps": 8,
  
  "optimizer": {
    "type": "AdamW",
    "params": {
      "lr": 0.001,
      "weight_decay": 5e-4
    }
  },
 
  "scheduler": {
      "type": "OneCycle",
      "params": {
          "cycle_min_lr": 1e-6,
          "cycle_max_lr": 1e-3,
          "decay_lr_rate": 1e-6
      }
  },
 
 "fp16": {
    "enabled": true,
    "loss_scale": 0,
    "initial_scale_power": 32,
    "loss_scale_window": 1000,
    "hysteresis": 2,
    "min_loss_scale": 1
    },

 "zero_allow_untested_optimizer": true
}
Overwriting ds_config.json

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [104]:
n_gpu = 4
command = f'dlojz.py --image-size {image_size} -p 4 --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 248512
jobid = ['248512']

Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [105]:
#jobid = ['230538']
In [106]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248512   gpu_p13   pseudo  cfor132  R       2:19      1 r6i7n6

 Done!
In [107]:
controle_technique(jobid)
Train throughput: 77.30 images/second
GPU throughput: 77.30 images/second
epoch time: 16573.70 seconds
training time estimation for 90 epochs (with validations): 420.70 hours
-----------
training step time average (fwd/bkwd on GPU): 2.483694 sec (nan%/nan%) +/- 0.064397
loading step time average (CPU to GPU): 0.000002 sec +/- 0.000000
-----------
ELIGIBLE to run 1 epochs
In [108]:
pipe_memory(jobid)

Optimisation du chunk number¶

Test d'occupation mémoire¶

Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.

In [109]:
import json
chunks_numbers = [2, 4, 8, 16, 32, 40]
for c in chunks_numbers:
    with open("ds_config.json", "r") as jsonFile:
        data = json.load(jsonFile)

    data["gradient_accumulation_steps"] = c

    with open(f"ds_config{c}.json", "w") as jsonFile:
        json.dump(data, jsonFile)

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [110]:
n_gpu = 4
command = [f'dlojz.py --image-size {image_size} -p 4 --test --deepspeed --deepspeed_config ds_config{c}.json'
           for c in chunks_numbers]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:20:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 248513
Submitted batch job 248514
Submitted batch job 248515
Submitted batch job 248516
Submitted batch job 248517
Submitted batch job 248518
jobids = ['248513', '248514', '248515', '248516', '248517', '248518']

Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.

Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.

In [111]:
#jobids = ['239664', '239666', '239667', '239668', '239674', '239676']
In [112]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248518   gpu_p13   pseudo  cfor132  R       7:05      1 r8i2n6

 Done!
In [113]:
GPU_underthehood(jobids, calcul_memo=True)
/gpfsdswork/projects/idris/for/cfor132/dlo-jz/dlojz_tools.py:250: FutureWarning:

elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison

Batch size per GPU: 48 Max GPU Memory Allocated: 11.67 GB, Troughput: 48.153 images/second
Batch size per GPU: 96 Max GPU Memory Allocated: 22.68 GB, Troughput: 62.772 images/second
Batch size per GPU: 192 Max GPU Memory Allocated: 22.79 GB, Troughput: 76.598 images/second
Batch size per GPU: 384 Max GPU Memory Allocated: 22.79 GB, Troughput: 83.476 images/second
Batch size per GPU: 768 Max GPU Memory Allocated: 22.79 GB, Troughput: 89.876 images/second
Batch size per GPU: 960 Max GPU Memory Allocated: 22.79 GB, Troughput: 91.305 images/second
Memory occupancy by Model part : 18.320 +/- 8.831 GB
In [114]:
controle_technique([jobids[-1]])
Train throughput: 91.31 images/second
GPU throughput: 91.31 images/second
epoch time: 14036.44 seconds
training time estimation for 90 epochs (with validations): 356.08 hours
-----------
training step time average (fwd/bkwd on GPU): 10.514183 sec (nan%/nan%) +/- 0.250693
loading step time average (CPU to GPU): 0.000003 sec +/- 0.000000
-----------
ELIGIBLE to run 1 epochs

Essais et recherche du meilleur parallèlisme¶

TODO : Trouver la meilleure architecture et configuration en terme de Throughput.

  • L'argument -p correspond au nombre de stages du pipeline. Sachant que l'on utilise 4 GPU, un stage de 4 correspond à un pipeline parallelism total sur 4 GPU, un stage de 2 correspond à un hybrid parallelism 2x2, un stage de 1 à un Data Parallelism complet.

  • Choisir un optimiseur accéléré comme : Adam, AdamW, Lamb, OnebitAdam, OnebitLamb, ou ZeroOneAdam.

Configuration JSON :

À noter :

  • Seul le stage 1 de ZeRO marche en hybrid parallelism avec Deepspeed.
  • OnebitAdam, OnebitLamb, ou ZeroOneAdam ne marche pas avec ZeRO. Si vous utilisez un de ceux-ci, il faudra mettre le paramètre freeze_step comme ceci pour pouvoir mesurer son accélération dans notre test :
"optimizer": {
    "type": "OnebitAdam",
    "params": {
      "lr": 0.001,
      "weight_decay": 5e-4,
      "freeze_step": 5
      }
  },
In [115]:
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 16,
  "gradient_accumulation_steps": 24,
  
  "optimizer": {
    "type": "AdamW",
    "params": {
      "lr": 0.001,
      "weight_decay": 5e-2
    }
  },
 
  "scheduler": {
      "type": "OneCycle",
      "params": {
          "cycle_min_lr": 1e-6,
          "cycle_max_lr": 1e-3,
          "decay_lr_rate": 1e-6
      }
  },
 
 "fp16": {
    "enabled": true,
    "loss_scale": 0,
    "initial_scale_power": 32,
    "loss_scale_window": 1000,
    "hysteresis": 2,
    "min_loss_scale": 1
    },
 
 
 "zero_optimization": {
    "stage": 1
 },
 "zero_allow_untested_optimizer": true
}
Overwriting ds_config.json

Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.

Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.

In [116]:
n_gpu = 4
command = f'dlojz.py --image-size {image_size} -p 2 --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
                   account=account, time_max='00:20:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task
Submitted batch job 248523
jobid = ['248523']

Copier-coller vos sorties jobid = ['xxxxx'] dans la cellule suivante.

In [117]:
#jobid = ['202876']
In [118]:
display_slurm_queue(name)
             JOBID PARTITION     NAME     USER ST       TIME  NODES NODELIST(REASON)
            248523   gpu_p13   pseudo  cfor132  R       5:25      1 r6i7n6

 Done!
In [119]:
controle_technique(jobid)
Train throughput: 98.05 images/second
GPU throughput: 98.05 images/second
epoch time: 13072.98 seconds
training time estimation for 90 epochs (with validations): 331.45 hours
-----------
training step time average (fwd/bkwd on GPU): 7.832820 sec (nan%/nan%) +/- 0.201737
loading step time average (CPU to GPU): 0.000002 sec +/- 0.000000
-----------
ELIGIBLE to run 1 epochs
In [120]:
pipe_memory(jobid)
In [ ]:
%cat {search_log(contains=jobid[0])[0]}
In [ ]:
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}

Commentaires